package it.tref.eclipse.wicket.plugin.editors;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.NodeFinder;
import org.eclipse.jdt.core.util.IModifierConstants;
import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility;
import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor;
import org.eclipse.jdt.internal.ui.search.BreakContinueTargetFinder;
import org.eclipse.jdt.internal.ui.search.ExceptionOccurrencesFinder;
import org.eclipse.jdt.internal.ui.search.IOccurrencesFinder.OccurrenceLocation;
import org.eclipse.jdt.internal.ui.search.ImplementOccurrencesFinder;
import org.eclipse.jdt.internal.ui.search.MethodExitsFinder;
import org.eclipse.jdt.internal.ui.search.OccurrencesFinder;
import org.eclipse.jdt.internal.ui.text.JavaWordFinder;
import org.eclipse.jdt.internal.ui.viewsupport.ISelectionListenerWithAST;
import org.eclipse.jdt.ui.SharedASTProvider;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension4;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ISelectionValidator;
import org.eclipse.jface.text.ISynchronizable;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.link.LinkedModeModel;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.IAnnotationModelExtension;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.texteditor.IDocumentProvider;

@SuppressWarnings("restriction")
public class WicketFunMarkOccurrenceInstaller {
	private ISelection fForcedMarkOccurrencesSelection;
	private IRegion fMarkOccurrenceTargetRegion;
	private OccurrencesFinderJob fOccurrencesFinderJob;
	private Annotation[] fOccurrenceAnnotations;

	private long fMarkOccurrenceModificationStamp;

	private boolean fMarkExceptions = true;
	private boolean fMarkTypeOccurrences = true;
	private boolean fMarkMethodExitPoints = true;
	private boolean fMarkBreakContinueTargets = true;
	private boolean fMarkImplementors = true;
	private boolean fMarkMethodOccurrences = true;
	private boolean fMarkConstantOccurrences = true;
	private boolean fMarkFieldOccurrences = true;
	private boolean fMarkLocalVariableypeOccurrences = true;
	private boolean fStickyOccurrenceAnnotations = false;

	private final JavaEditor editor;
	private ISelectionListenerWithAST fPostSelectionListenerWithAST;

	public WicketFunMarkOccurrenceInstaller(JavaEditor editor) {
		this.editor = editor;
	}

	public void installOccurrencesFinder(boolean forceUpdate) {
		fPostSelectionListenerWithAST = new ISelectionListenerWithAST()
		{
			public void selectionChanged(IEditorPart part, ITextSelection selection, CompilationUnit astRoot)
			{
				if(editor.getViewer() == null)
				{
					SelectionListenerWithASTManager.getDefault().removeListener(editor, fPostSelectionListenerWithAST);
				}
				else
				{
					updateOccurrenceAnnotations(selection, astRoot, editor);
				}
			}
		};

		SelectionListenerWithASTManager.getDefault().addListener(editor,
				fPostSelectionListenerWithAST);
		
		if (forceUpdate && editor.getSelectionProvider() != null) {
			fForcedMarkOccurrencesSelection = editor.getSelectionProvider()
					.getSelection();
			updateOccurrenceAnnotations(
					(ITextSelection) fForcedMarkOccurrencesSelection,
					JavaPlugin
							.getDefault()
							.getASTProvider()
							.getAST(EditorUtility.getEditorInputJavaElement(
									editor, false), SharedASTProvider.WAIT_YES,
									null), editor);
		}

		/*
		 * if (fOccurrencesFinderJobCanceler == null) {
		 * fOccurrencesFinderJobCanceler= new OccurrencesFinderJobCanceler();
		 * fOccurrencesFinderJobCanceler.install(); }
		 */
	}

	private void updateOccurrenceAnnotations(ITextSelection selection,
			CompilationUnit astRoot, JavaEditor editor) {
		if (fOccurrencesFinderJob != null)
			fOccurrencesFinderJob.cancel();

		/*
		 * if (!fMarkOccurrenceAnnotations) return;
		 */

		if (astRoot == null || selection == null)
			return;

		IDocument document = editor.getViewer().getDocument();
		if (document == null)
			return;

		if (document instanceof IDocumentExtension4) {
			int offset = selection.getOffset();
			long currentModificationStamp = ((IDocumentExtension4) document)
					.getModificationStamp();
			IRegion markOccurrenceTargetRegion = fMarkOccurrenceTargetRegion;
			if (markOccurrenceTargetRegion != null
					&& currentModificationStamp == fMarkOccurrenceModificationStamp) {
				if (markOccurrenceTargetRegion.getOffset() <= offset
						&& offset <= markOccurrenceTargetRegion.getOffset()
								+ markOccurrenceTargetRegion.getLength())
					return;
			}
			fMarkOccurrenceTargetRegion = JavaWordFinder.findWord(document,
					offset);
			fMarkOccurrenceModificationStamp = currentModificationStamp;
		}

		OccurrenceLocation[] matches = null;

		ASTNode selectedNode = NodeFinder.perform(astRoot,
				selection.getOffset(), selection.getLength());

		if (fMarkExceptions || fMarkTypeOccurrences) {
			ExceptionOccurrencesFinder exceptionFinder = new ExceptionOccurrencesFinder();
			String message = exceptionFinder.initialize(astRoot, selectedNode);
			if (message == null) {
				matches = exceptionFinder.getOccurrences();
				if (!fMarkExceptions && matches != null)
					matches = null;
			}
		}

		if ((matches == null)
				&& (fMarkMethodExitPoints || fMarkTypeOccurrences)) {
			MethodExitsFinder finder = new MethodExitsFinder();
			String message = finder.initialize(astRoot, selectedNode);
			if (message == null) {
				matches = finder.getOccurrences();
				if (!fMarkMethodExitPoints && matches != null)
					matches = null;
			}
		}

		if ((matches == null)
				&& (fMarkBreakContinueTargets || fMarkTypeOccurrences)) {
			BreakContinueTargetFinder finder = new BreakContinueTargetFinder();
			String message = finder.initialize(astRoot, selectedNode);
			if (message == null) {
				matches = finder.getOccurrences();
				if (!fMarkBreakContinueTargets && matches != null)
					matches = null;
			}
		}

		if ((matches == null) && (fMarkImplementors || fMarkTypeOccurrences)) {
			ImplementOccurrencesFinder finder = new ImplementOccurrencesFinder();
			String message = finder.initialize(astRoot, selectedNode);
			if (message == null) {
				matches = finder.getOccurrences();
				if (!fMarkImplementors && matches != null)
					matches = null;
			}
		}

		if (matches == null) {
			IBinding binding = null;
			if (selectedNode instanceof Name)
				binding = ((Name) selectedNode).resolveBinding();

			if (binding != null && markOccurrencesOfType(binding)) {
				// Find the matches && extract positions so we can forget the
				// AST
				OccurrencesFinder finder = new OccurrencesFinder();
				String message = finder.initialize(astRoot, selectedNode);
				if (message == null)
					matches = finder.getOccurrences();
			}
		}

		if (matches == null || matches.length == 0) {
			if (!fStickyOccurrenceAnnotations)
				removeOccurrenceAnnotations(editor);
			return;
		}

		Position[] positions = new Position[matches.length];
		int i = 0;
		for (int c = 0; c < matches.length; c++) {
			positions[i++] = new Position(matches[c].getOffset(),
					matches[c].getLength());
		}

		fOccurrencesFinderJob = new OccurrencesFinderJob(document, positions,
				selection, editor);
		fOccurrencesFinderJob.run(new NullProgressMonitor());
	}

	private void removeOccurrenceAnnotations(JavaEditor editor) {
		fMarkOccurrenceModificationStamp = IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
		fMarkOccurrenceTargetRegion = null;

		IDocumentProvider documentProvider = editor.getDocumentProvider();
		if (documentProvider == null)
			return;

		IAnnotationModel annotationModel = documentProvider
				.getAnnotationModel(editor.getEditorInput());
		if (annotationModel == null || fOccurrenceAnnotations == null)
			return;

		synchronized (getLockObject(annotationModel)) {
			if (annotationModel instanceof IAnnotationModelExtension) {
				((IAnnotationModelExtension) annotationModel)
						.replaceAnnotations(fOccurrenceAnnotations, null);
			} else {
				for (int i = 0, length = fOccurrenceAnnotations.length; i < length; i++)
					annotationModel.removeAnnotation(fOccurrenceAnnotations[i]);
			}
			fOccurrenceAnnotations = null;
		}
		
		
	}

	private class OccurrencesFinderJob extends Job {
		private IDocument fDocument;
		private ISelection fSelection;
		private ISelectionValidator fPostSelectionValidator;
		private boolean fCanceled = false;
		private IProgressMonitor fProgressMonitor;
		private Position[] fPositions;

		private JavaEditor editor;

		public OccurrencesFinderJob(IDocument document, Position[] positions,
				ISelection selection, JavaEditor editor) {
			super("WicketFun_MarkOccurrencesJob");

			fDocument = document;
			fSelection = selection;
			fPositions = positions;
			this.editor = editor;

			if (editor.getSelectionProvider() instanceof ISelectionValidator)
				fPostSelectionValidator = (ISelectionValidator) editor
						.getSelectionProvider();
		}

		// cannot use cancel() because it is declared final
		@SuppressWarnings("unused")
		private void doCancel() {
			fCanceled = true;
			cancel();
		}

		private boolean isCanceled() {
			return fCanceled
					|| fProgressMonitor.isCanceled()
					|| fPostSelectionValidator != null
					&& !(fPostSelectionValidator.isValid(fSelection) || fForcedMarkOccurrencesSelection == fSelection)
					|| LinkedModeModel.hasInstalledModel(fDocument);
		}

		/*
		 * @see Job#run(org.eclipse.core.runtime.IProgressMonitor)
		 */
		@SuppressWarnings({ "rawtypes", "unchecked" })
		public IStatus run(IProgressMonitor progressMonitor) {
			fProgressMonitor = progressMonitor;

			if (isCanceled())
				return Status.CANCEL_STATUS;

			ITextViewer textViewer = editor.getViewer();
			if (textViewer == null)
				return Status.CANCEL_STATUS;

			IDocument document = textViewer.getDocument();
			if (document == null)
				return Status.CANCEL_STATUS;

			IDocumentProvider documentProvider = editor.getDocumentProvider();
			if (documentProvider == null)
				return Status.CANCEL_STATUS;

			IAnnotationModel annotationModel = documentProvider
					.getAnnotationModel(editor.getEditorInput());
			if (annotationModel == null)
				return Status.CANCEL_STATUS;

			// Add occurrence annotations
			int length = fPositions.length;

			Map annotationMap = new HashMap(length);
			for (int i = 0; i < length; i++) {
				if (isCanceled())
					return Status.CANCEL_STATUS;

				String message;
				Position position = fPositions[i];

				// Create & add annotation
				try {
					message = document.get(position.offset, position.length);
				} catch (BadLocationException ex) {
					// Skip this match
					continue;
				}

				annotationMap.put(new Annotation(
						"org.eclipse.jdt.ui.occurrences", false, message), //$NON-NLS-1$
						position);
			}

			if (isCanceled())
				return Status.CANCEL_STATUS;

			synchronized (getLockObject(annotationModel)) {
				if (annotationModel instanceof IAnnotationModelExtension) {
					((IAnnotationModelExtension) annotationModel)
							.replaceAnnotations(fOccurrenceAnnotations,
									annotationMap);
				} else {
					// removeOccurrenceAnnotations();
					Iterator iter = annotationMap.entrySet().iterator();
					while (iter.hasNext()) {
						Map.Entry mapEntry = (Map.Entry) iter.next();
						annotationModel.addAnnotation(
								(Annotation) mapEntry.getKey(),
								(Position) mapEntry.getValue());
					}
				}
				fOccurrenceAnnotations = (Annotation[]) annotationMap.keySet()
						.toArray(new Annotation[annotationMap.keySet().size()]);
			}

			return Status.OK_STATUS;
		}
	}

	private Object getLockObject(IAnnotationModel annotationModel) {
		if (annotationModel instanceof ISynchronizable) {
			Object lock = ((ISynchronizable) annotationModel).getLockObject();
			if (lock != null)
				return lock;
		}
		return annotationModel;
	}

	boolean markOccurrencesOfType(IBinding binding) {
		if (binding == null)
			return false;

		int kind = binding.getKind();

		if (fMarkTypeOccurrences && kind == IBinding.TYPE)
			return true;

		if (fMarkMethodOccurrences && kind == IBinding.METHOD)
			return true;

		if (kind == IBinding.VARIABLE) {
			IVariableBinding variableBinding = (IVariableBinding) binding;
			if (variableBinding.isField()) {
				int constantModifier = IModifierConstants.ACC_STATIC
						| IModifierConstants.ACC_FINAL;
				boolean isConstant = (variableBinding.getModifiers() & constantModifier) == constantModifier;
				if (isConstant)
					return fMarkConstantOccurrences;
				else
					return fMarkFieldOccurrences;
			}

			return fMarkLocalVariableypeOccurrences;
		}

		return false;
	}
}